import * as os from 'os';
import * as fs from 'fs-extra';
import path from 'path';

import { Storage } from '$infra';
import { storageCLI } from '$infra/storage/storage-cli';
import { agentConfig } from '$domain/model/agent/config';
import { logger, isZip, extractZipTo, generateUUID } from '$common';
export interface ModelZipHandler {
  unzipToModelDir(modelPath: string, localModelDir: string): Promise<string>;
}

type ModelDownloadRecord = {
  remoteZipPath: string;
  lastModified: Date;
  localZipPath: string;
}

const downloadCache: { [RemoteZipPath: string]: ModelDownloadRecord } = {};

export class RemoteModelHandler implements ModelZipHandler {

  private async download(remoteZipPath: string, localZipPath: string) {
    if (agentConfig.storageCLIEnable) {
      await storageCLI.fGetObject(remoteZipPath, localZipPath);
    } else {
      await Storage.Instance.fGetObject(remoteZipPath, localZipPath);
    }
    if (!isZip(localZipPath)) {
      throw new Error(`🔥 Local simulation zip file(${localZipPath}) downloaded from ${remoteZipPath} is not a zip file.`);
    }
  }

  private async isChanged(lastModified1: Date, lastModified2: Date) {
    return lastModified1.getTime() !== lastModified2.getTime();
  }
  
  async unzipToModelDir(remoteZipPath: string, localModelDir: string): Promise<string> {
    let localZipPath = path.join(os.tmpdir(), generateUUID());

    const record = downloadCache[remoteZipPath];
    if (record && fs.pathExistsSync(record.localZipPath)) {
      const remoteLastModified = await Storage.Instance.getLastModified(remoteZipPath);
      const zipChanged = await this.isChanged(remoteLastModified, record.lastModified);
      if (zipChanged) {
        logger.debug(`model zip changed. downloading from ${remoteZipPath} to ${localZipPath}`);
        await this.download(record.remoteZipPath, localZipPath);

        downloadCache[remoteZipPath] = {
          localZipPath,
          remoteZipPath: record.remoteZipPath,
          lastModified: remoteLastModified
        };

        logger.debug(`🆑 rm local model zip: ${record.localZipPath}`);

        fs.remove(record.localZipPath)
          .then(() => {
            logger.debug(`model zip changed. remove older one: ${record.localZipPath}`);
          })
          .catch(e => {
            logger.warn(`model zip changed. remove older one: ${record.localZipPath}} failed. ${e}`);
          });
      } else {
        localZipPath = record.localZipPath;
        logger.info(`🌳 Model file unchanged, reuse model zip ${record.localZipPath}, skip download.`);
      }
    } else {
      logger.debug(`🌈 Fetching model zip from storage path: ${remoteZipPath} to local ${localZipPath}`);
      await this.download(remoteZipPath, localZipPath);
      logger.debug('🌈 download done.');

      const remoteLastModified = await Storage.Instance.getLastModified(remoteZipPath);
      downloadCache[remoteZipPath] = {
        remoteZipPath,
        localZipPath,
        lastModified: remoteLastModified
      };
    }

    logger.debug(`🌈 (unzip) ==> from ${localZipPath} to ${localModelDir}`);
    await extractZipTo(localZipPath, localModelDir);
    logger.debug(`🌈 (unzip done) ==> from ${localZipPath} to ${localModelDir}`);

    return localModelDir;
  }
}

export class LocalModelHandler implements ModelZipHandler {

  async unzipToModelDir(localModelZipPath: string, localModelDir: string): Promise<string> {
    if (isZip(localModelZipPath)) {
      await extractZipTo(localModelZipPath, localModelDir);
      logger.debug(`(local) ==> Unzipped ${localModelZipPath} to ${localModelDir}`);
      return localModelDir;
    } 
    const stats = await fs.lstat(localModelZipPath);

    if (!stats.isDirectory()) {
      throw new Error(`modelLocationType is 'local', localModelPath: ${localModelZipPath} must be directory.`);
    }
    return localModelZipPath;
  }
}